<?php  if (! defined('BASEPATH')) exit('No direct script access allowed');

/**
 * Simple Model for CodeIgniter
 *
 * @author		Alexander Wenzel (alexander.wenzel.berlin@gmail.com)
 * @see			https://bitbucket.org/alexwenzel/codeigniter-model
 *
 * @package		CodeIgniter Core
 * @version		1.2.0
 * @license		BSD
 *
 */
class SMC_Model extends CI_Model implements Iterator {
	
	protected $table			= NULL;		// concrete model values (these will be overrided by concrete model)
	protected $primary 			= NULL;		// concrete model values (these will be overrided by concrete model)
	
	protected $class;						// holds the called class
	
	protected $position;
	protected $results			= array();	// holds the results of the last query
	protected $results_count 	= 0;		// holds the number of results of the last query
	
	
	/**
	 * Bootstrap of SMC_Model
	 *
	 * If any object is passed to constructor, the model will populate 
	 * itself with all valid properties. 
	 * 
	 * @author		Alexander Wenzel
	 * 
	 * @param		mixed			$values
	 * @return		object
	 */
	function __construct($values = NULL)
	{
		// stop whole process if the calling class is this class itself
		// this is because codeigniter
		if (get_called_class() === __CLASS__) return;
		
		// save all properties
		$this->class = $class = get_called_class();
		
		// check if a tablename isset
		// if not its the typical plural of our classname
		if ( ! isset($this->table) || ! is_string($this->table)) {
			$this->table = $this->guess_tablename();
		}
		
		// check if a primary key is set
		// if not its the typical id column
		if ( ! isset($this->primary) || ! is_string($this->primary)) {
			$default_key 	= 'id';
			$this->primary 	= $default_key;
			
			// if a default property is already set up dont do it again
			if ( ! isset($this->$default_key)) {
				$this->$default_key = NULL;
			}
		}
		
		// load values if any are passed to constructor
		if ($values !== NULL) {
			// by Philipp Tempel
			// Access object attributes and array keys the same way
			foreach ($values as $key => $value)
			{
				$this->$key = $value;
			}
		}
	}
	
	
	/**
	 * Returns a string presentation of this model
	 * 
	 * This method allows a class to decide how it will react when it is treated like a string.
	 *
	 * @author		Alexander Wenzel
	 * 
	 * @return		string
	 */
	function __toString()
	{
		$return = "model ({$this->class}) : ";
		
		foreach ($this->get_my_properties() as $property => $value) {
				$return .= "[{$property} = {$value}] ";
		}
		
		return trim($return);
	}
	
	
	/**
	 * Populates the Model with the result of a query
	 * Is called after each get()
	 * 
	 * @author		Alexander Wenzel
	 *
	 * @param		resource		$query
	 * @return		void
	 */
	protected function process_results($query)
	{
		// get called class
		$class = get_called_class();
		
		// in case there is no result, return a fresh instance
		if ($query->num_rows() === 0) {
			return new $class();
		}
		
		// reset results array
		$this->results = array();
		
		// fill result array
		foreach ($query->result() as $result) {
			// create new instance of this model
			$this->results[] = new $class($result);
		}
		
		// populate this instance with first result
		foreach ($query->row() as $key => $value) {
			$this->$key = $value;
		}
		
		// setting result count
		$this->results_count = $query->num_rows();
		
		// free result
		$query->free_result();
	}
	
	
	/**
	 * Returns all public properties of the current class
	 *
	 * @author     	joelhy
	 * @see			http://php.net/manual/en/function.get-object-vars.php#98394
	 * 
	 * @return     	array 			array with public properties
	 */
	private function get_my_properties()
	{
		$ref = new ReflectionObject($this);
		
		$pros = $ref->getProperties(ReflectionProperty::IS_PUBLIC);
		
		$result = array();
		
		foreach ($pros as $pro) {
			false && $pro = new ReflectionProperty();
			$result[$pro->getName()] = $pro->getValue($this);
		}
	
		return $result;
	}
	
	
	/**
	 * Fetches the table from the pluralised model name.
	 *
	 * @author Jamie Rumbelow
	 * @author Philipp Tempel
	 * 
	 * @return string
	 */
	private function guess_tablename()
	{
		// Strip "_m" or "_model" from the end of the class name (some people prefer
		//  using such model names)
		$class = preg_replace('/(_m|_model)?$/i', '', get_class($this));
		
		// Load the inflector-helper
		get_instance()->load->helper('inflector');
		
		// Pluralize the class-name
		return plural(strtolower($class));
	}
	
	
	/**
	 * Performs a SELECT statement on this model and returns the result
	 * 
	 * @author		Alexander Wenzel
	 *
	 * @param		string			$column
	 * @param		string			$value
	 * @param		int				$limit
	 * @return		object
	 */
	public static function find($column, $value, $limit = NULL)
	{
		$class = get_called_class();
		
		$return = new $class();
		$return->where($column, $value);
		
		if ($limit !== NULL) {
			$return->limit($limit);
		}
		
		$return->get();
		
		return $return;
	}
	
	
	/**
	 * Returns a new instance of this model, which is capable for method chaining
	 * 
	 * Ability to use for method chaining
	 * 
	 * @author		Alexander Wenzel
	 *
	 * @return		object
	 */
	public static function query()
	{
		$class = get_called_class();
		
		$return = new $class();
		
		return $return;
	}
	
	
	/**
	 * Returns all results for this model
	 * 
	 * @author		Alexander Wenzel
	 *
	 * @return		object
	 */
	public static function all()
	{
		$class = get_called_class();
		
		$return = new $class();
		$return->get();
		
		return $return;
	}
	
	
	/**
	 * Returns the number of all records for this model
	 * 
	 * @author		Alexander Wenzel
	 *
	 * @return		int
	 */
	public static function count()
	{
		$class = get_called_class();
		
		$return = new $class();
		return $return->count_all();
	}
	
	
	/**
	 * Forces the model to save data to the table as a new record
	 * 
	 * Auto-populates the last insert id to the primary key.
	 * 
	 * If an array with property names is passed, only save those properties.
	 * Otherwise we assume it shall save all public properties.
	 * 
	 * @author		Alexander Wenzel
	 * 
	 * @param		array			$properties_to_save
	 * @return		bool			success of operation
	 */
	public function create($properties_to_save = array())
	{
		// getting our primary key
		$key = $this->primary;
		
		// remove primary key to force the new record
		if (property_exists($this, $key)) {
			unset($this->$key);
		}
		
		// delegate to save()
		return $this->save($properties_to_save);
	}
	
	
	/**
	 * Forces the model to save data to the table as a new record
	 * 
	 * Auto-populates the last insert id to the primary key.
	 * 
	 * If an array with property names is passed, only save those properties.
	 * Otherwise we assume it shall save all public properties.
	 * 
	 * @author		Alexander Wenzel
	 * 
	 * @deprecated	will be removed in next version
	 * 
	 * @param		array			$properties_to_save
	 * @return		bool			success of operation
	 */
	public function save_new($properties_to_save = array())
	{
		// delegate to new create() method
		return $this->save_new($properties_to_save);
	}
	
	
	/**
	 * Saves the model to the table
	 * 
	 * Auto-populates the last insert id if it's an insert.
	 * 
	 * If an array with property names is passed, only save those properties.
	 * Otherwise we assume it shall save all public properties.
	 * 
	 * @author		Alexander Wenzel
	 * 
	 * @param		array			$properties_to_save
	 * @return		bool			success of operation
	 */
	public function save($properties_to_save = array())
	{
		// create clean object for pushing to database
		$db_object = new stdClass();
		
		// getting our primary key
		$key = $this->primary;
		
		// if an array is passed, only save those properties
		if ( ! empty($properties_to_save) && is_array($properties_to_save))	{
			
			// if its an update statement and our primary key isn't in our save array, push it in
			if (isset($this->$key) && ! empty($this->$key) && ! in_array($key, $properties_to_save)) {
				array_push($properties_to_save,$key);
			}
			
			// populate our clean db_object with its properties to save
			foreach ($properties_to_save as $field) {
				$db_object->$field = $this->$field;
			}
		}
		
		// if nothing is passed, we assume we shall save all public properties
		else {
			// populate our clean db_object with its public properties
			foreach ($this->get_my_properties() as $property => $value) {
				$db_object->$property = $value;
			}
		}
		
		// now our save begins
		
		// if primary key is not empty update data
		if (property_exists($db_object, $key) && ! empty($db_object->$key)) {
			$this->db->where($key, $db_object->$key);
			
			return $this->db->update($this->table, $db_object);
		}
		// otherwise insert them and populate the new insert id
		else {
			if ($this->db->insert($this->table, $db_object))	{
				$this->$key = $this->db->insert_id();
				
				return TRUE;
			}
			
			return FALSE;
		}
	}
	
	
	/**
	 * Removes the row represented by this instance of the model from table
	 * 
	 * @author		Alexander Wenzel
	 * 
	 * @return		bool			success of operation
	 */
	public function remove()
	{
		// getting our primary key
		$key = $this->primary;
		
		if (isset($this->$key) && ! empty($this->$key)) {
			return $this->where($key, $this->$key)->delete();
		}
		
		return FALSE;
	}
	
	
	/**
	 * Returns results from a query
	 * 
	 * If an offset is given its returns the specific result row.
	 * If the offset doesn't exists, it returns FALSE.
	 * 
	 * @author		Alexander Wenzel
	 * 
	 * @param		int				$offset
	 * @return		array			results of last action
	 */
	public function result($offset = NULL)
	{
		if ($offset !== NULL && is_int($offset)) {
			if (array_key_exists($offset, $this->results)) {
				return $this->results[$offset];
			}
			
			return FALSE;
		}
		
		return $this->results;
	}
	
	
	/**
	 * Returns the current model as JSON string
	 * 
	 * @author		Alexander Wenzel
	 * 
	 * @return		string
	 */
	public function to_json()
	{
		return json_encode($this);
	}
	
	
	/**
	 * SELECT Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$fields
	 * @return		object
	 */
	public function select($fields = '*')
	{
		$this->db->select($fields);
		return $this;
	}
	
	
	/**
	 * SELECT MAX(field) Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		string			$rename
	 * @return		object
	 */
	public function select_max($field, $rename = NULL)
	{
		$this->db->select_max($field, $rename);
		return $this;
	}
	
	
	/**
	 * SELECT MIN(field) Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		string			$rename
	 * @return		object
	 */
	public function select_min($field, $rename = NULL)
	{
		$this->db->select_min($field, $rename);
		return $this;
	}
	
	
	/**
	 * SELECT AVG(field) Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		string			$rename
	 * @return		object
	 */
	public function select_avg($field, $rename = NULL)
	{
		$this->db->select_avg($field, $rename);
		return $this;
	}
	
	
	/**
	 * SELECT SUM(field) Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		string			$rename
	 * @return		object
	 */
	public function select_sum($field, $rename = NULL)
	{
		$this->db->select_sum($field, $rename);
		return $this;
	}
	
	
	/**
	 * DISTINCT Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @return		object
	 */
	public function distinct()
	{
		$this->db->distinct();
		return $this;
	}
	
	
	/**
	 * WHERE Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		string			$value
	 * @return		object
	 */
	public function where($field, $value)
	{
		$this->db->where($field, $value);
		return $this;
	}
	
	
	/**
	 * WHERE Statement with OR
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		string			$value
	 * @return		object
	 */
	public function or_where($field, $value)
	{
		$this->db->or_where($field, $value);
		return $this;
	}
	
	
	/**
	 * WHERE IN Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		array			$values
	 * @return		object
	 */
	public function where_in($field, $values)
	{
		$this->db->where_in($field, $value);
		return $this;
	}
	
	
	/**
	 * OR WHERE IN Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		array			$values
	 * @return		object
	 */
	public function or_where_in($field, $values)
	{
		$this->db->or_where_in($field, $value);
		return $this;
	}
	
	
	/**
	 * WHERE NOT IN Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		array			$values
	 * @return		object
	 */
	public function where_not_in($field, $values)
	{
		$this->db->where_not_in($field, $value);
		return $this;
	}
	
	
	/**
	 * OR WHERE NOT IN Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		array			$values
	 * @return		object
	 */
	public function or_where_not_in($field, $values)
	{
		$this->db->or_where_not_in($field, $value);
		return $this;
	}
	
	
	/**
	 * LIKE Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		string			$match
	 * @param		string			$use_wildcards
	 * @return		object
	 */
	public function like($field, $match, $use_wildcards = 'both')
	{
		$this->db->like($field, $match, $use_wildcards);
		return $this;
	}
	
	
	/**
	 * OR LIKE Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		string			$match
	 * @param		string			$use_wildcards
	 * @return		object
	 */
	public function or_like($field, $match, $use_wildcards = 'both')
	{
		$this->db->or_like($field, $match, $use_wildcards);
		return $this;
	}
	
	
	/**
	 * NOT LIKE Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		string			$match
	 * @param		string			$use_wildcards
	 * @return		object
	 */
	public function not_like($field, $match, $use_wildcards = 'both')
	{
		$this->db->not_like($field, $match, $use_wildcards);
		return $this;
	}
	
	
	/**
	 * OR NOT LIKE Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		string			$match
	 * @param		string			$use_wildcards
	 * @return		object
	 */
	public function or_not_like($field, $match, $use_wildcards = 'both')
	{
		$this->db->or_not_like($field, $match, $use_wildcards);
		return $this;
	}
	
	
	/**
	 * GROUP BY Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		mixed			$fields
	 * @return		object
	 */
	public function group_by($fields)
	{
		$this->db->group_by($fields);
		return $this;
	}
	
	
	/**
	 * ORDER BY Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string			$field
	 * @param		string			$order
	 * @return		object
	 */
	public function order_by($field, $order = 'ASC')
	{
		$this->db->order_by($field, $order);
		return $this;
	}
	
	
	/**
	 * JOIN Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		string	$table		name of the joining table
	 * @param		string	$condition	joining condition
	 * @param		string	$type		join type (left, right, outer, inner, left outer, and right outer)
	 * @return		object
	 */
	public function join($table, $condition, $type = 'left')
	{
		$this->db->join($table, $condition, $type);
		return $this;
	}
	
	
	/**
	 * LIMIT Statement
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @param		int		$limit
	 * @param		int		$offset
	 * @return		object
	 */
	public function limit($limit, $offset = 0)
	{
		$this->db->limit($limit, $offset);
		return $this;
	}
	
	
	/**
	 * Runs the selection query and populates the model with the the result(s)
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @return		bool			success of operation
	 */
	public function get()
	{
		try {
			$query = $this->db->get($this->table);
			$this->process_results($query);
			return $this;
		}
		catch (exception $ex) {
			return FALSE;
		}
	}
	
	
	/**
	 * Generates a delete SQL string and runs the query
	 * 
	 * Used for model method chaining
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @return		void
	 */
	public function delete()
	{
		return $this->db->delete($this->table);
	}
	
	
	/**
	 * Permits you to determine the number of rows in a particular table
	 *
	 * @author		Alexander Wenzel
	 * @see			http://codeigniter.com/user_guide/database/active_record.html
	 *
	 * @return		int				number of rows
	 */
	public function count_all()
	{
		return $this->db->count_all($this->table);
	}
	
	
	/**
	 * Implementation of iterator
	 * 
	 * @author		Alexander Wenzel
	 *
	 * @return		object
	 */
	public function rewind()
	{
		$this->position = 0;
	}
	
	
	/**
	 * Implementation of iterator
	 * 
	 * @author		Alexander Wenzel
	 *
	 * @return		object
	 */
	public function current()
	{
		return $this->results[$this->position];
	}
	
	
	/**
	 * Implementation of iterator
	 * 
	 * @author		Alexander Wenzel
	 *
	 * @return		object
	 */
	public function key()
	{
		return $this->position;
	}
	
	
	/**
	 * Implementation of iterator
	 * 
	 * @author		Alexander Wenzel
	 *
	 * @return		object
	 */
	public function next()
	{
		$this->position++;
	}
	
	
	/**
	 * Implementation of iterator
	 * 
	 * @author		Alexander Wenzel
	 *
	 * @return		object
	 */
	public function valid()
	{
		return $this->position < count($this->results);
	}
}